// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * @fileoverview Define the controller for bite.layer.Status console.  The
 * object defines the functionality specifically for this controller and does
 * not affect the outside world.  However, it does define a set of signals that
 * can be used by external sources to perform actions when the controller
 * performs certain tasks.
 *
 * The Status console is intended to have only a single instance.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('bite.layer.Status');
goog.provide('bite.layer.StatusEventType');

goog.require('bite.common.events.DataBearingEvent');
goog.require('bite.common.mvc.helper');
goog.require('bite.layer.ConsoleBehavior');
goog.require('bite.layer.soy.Status');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');



/**
 * Constructs an instance of a Status controller.  The constructor creates the
 * object in a default state that has not been mapped to a view.
 * Note: As I need to use goog.mixin to fake multiple inheritance, I believe I
 *       can only extend the console once, so I am using EventTarget for that.
 * @extends {goog.events.EventTarget}
 * @constructor
 * @export
 */
bite.layer.Status = function() {
  goog.base(this);

  /**
   * The base url for all relative data.
   * @type {string}
   * @private
   */
  this.baseUrl_ = '';

  /**
   * The console element.
   * @type {Element}
   * @private
   */
  this.console_ = null;

  /**
   * Whether or not the console's view is initialized.
   * @type {boolean}
   * @private
   */
  this.isViewInitialized_ = false;

  /**
   * Whether or not the console's controller is initialized.
   * @type {boolean}
   * @private
   */
  this.isModelInitialized_ = false;

  /**
   * The tbody element of the table representing the layers.
   * @type {Element}
   * @private
   */
  this.tbody_ = null;
};
goog.inherits(bite.layer.Status, goog.events.EventTarget);
goog.mixin(bite.layer.Status.prototype, bite.layer.ConsoleBehavior.prototype);
goog.addSingletonGetter(bite.layer.Status);


/**
 * The console id.
 * @type {string}
 * @private
 */
bite.layer.Status.CONSOLE_ID_ = 'layer-status-console';


/**
 * Actions for external response.
 * @enum {string}
 * @private
 */
bite.layer.Status.Action_ = {
  CHANGE_ONDEMAND: 'change-ondemand',
  CHANGE_STATE: 'change-state',
  RESET: 'reset'
};


/**
 * Button ids for the console.
 * @enum {string}
 * @private
 */
bite.layer.Status.Button_ = {
  CLOSE: 'layer-status-close',
  RESET_MANAGER: 'layer-status-reset-manager',
  RESTORE_DEFAULTS: 'layer-status-restore'
};


/**
 * Defines key values used to pass data in and out of the console.
 * @enum {string}
 * @export
 */
bite.layer.Status.Data = {
  ACTION: 'action',
  DATA_STATUS: 'dataStatus',
  ID: 'id',
  ONDEMAND: 'ondemand',
  LOAD_IMMEDIATE: 'loadImmediate',
  STATE: 'state',
  STATUS: 'status',
  VALUE: 'value'
};


/**
 * Input buttons ids for the console.
 * @enum {string}
 * @private
 */
bite.layer.Status.InputBase_ = {
  IMAGE: 'layer-status-image-',
  ONDEMAND: 'layer-status-ondemand-',
  RESET: 'layer-status-reset-',
  STATE: 'layer-status-state-'
};


/**
 * Layer status values.
 * @enum {string}
 * @private
 */
bite.layer.Status.LayerStatus_ = {
  BAD_OBJECT: 'bad-object',
  ERROR: 'error',
  FAILED: 'failed',
  INIT: 'init',
  LOADED: 'loaded',
  LOADING: 'loading',
  UNLOADED: 'unloaded',
  WARNING: 'warning'
};


/**
 * Element ids for general elements.
 * @enum {string}
 * @private
 */
bite.layer.Status.Other_ = {
  STATUS: 'layer-status-state-'
};


/**
 * Radio button mappings to layer appropriate values.
 * @enum {string}
 * @private
 */
bite.layer.Status.State_ = {
  ALWAYS: 'fixed',
  NEVER: 'fixed',
  OFF: 'off',
  ON: 'on'
};


/**
 * Element ids for the console.
 * @enum {string}
 * @private
 */
bite.layer.Status.Table_ = {
  TBODY: 'layer-status-tbody',
  TR: 'layer-status-tr-'
};


/**
 * Event types supported by this console.
 * @enum {string}
 * @export
 */
bite.layer.StatusEventType = {
  CLOSE: 'close',
  RESET: 'reset',
  RESET_MANAGER: 'reset-manager',
  RESTORE_DEFAULTS: 'restore',
  SET_ONDEMAND: 'setOnDemand',
  SET_STATE: 'setState'
};


/**
 * Create a new row element with the given id.
 * @param {string} id The id of the layer.
 * @param {!Element} tbody The table body element the row will belong to.
 * @return {Element} The new element or null.
 * @private
 */
bite.layer.Status.prototype.createRow_ = function(id, tbody) {
  // The row element must be created manually rather than letting soy create
  // it because soy uses DIVs by default and setting the innerHTML of a DIV
  // to a TR element will remove all table related tags from the rendered
  // output.
  var row = goog.dom.createElement(goog.dom.TagName.TR);
  row.id = bite.layer.Status.Table_.TR + id;

  // The row must be appended into a table prior to rendering or the browser
  // will strip out all table related tags.
  goog.dom.appendChild(tbody, row);

  var helper = bite.common.mvc.helper;
  var data = {'baseUrl': this.baseUrl_, 'id': id};
  if (!helper.renderModelFor(row, bite.layer.soy.Status.createRow, data)) {
    goog.dom.removeNode(/** @type {Node} */ (row));
    return null;
  }

  var inputBase = bite.layer.Status.InputBase_;
  var ondemand = helper.getElement(inputBase.ONDEMAND + id, row);
  var reset = helper.getElement(inputBase.RESET + id, row);
  var state = helper.getElement(inputBase.STATE + id, row);
  if (goog.isNull(ondemand) || goog.isNull(reset) || goog.isNull(state)) {
    goog.dom.removeNode(/** @type {Node} */ (row));
    return null;
  }

  goog.events.listen(ondemand, goog.events.EventType.CLICK,
                     goog.bind(this.handleSetOndemand_, this, id));
  goog.events.listen(reset, goog.events.EventType.CLICK,
                     goog.bind(this.handleOnReset_, this, id));
  goog.events.listen(state, goog.events.EventType.CLICK,
                     goog.bind(this.handleSetState_, this, id));

  return row;
};


/**
 * Handler that is called when the close button is pressed.
 * @private
 */
bite.layer.Status.prototype.handleOnClose_ = function() {
  var type = bite.layer.StatusEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.CLOSE);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the close button is pressed.
 * @param {string} id The layer id.
 * @private
 */
bite.layer.Status.prototype.handleOnReset_ = function(id) {
  var data = {};
  data[bite.layer.Status.Data.ID] = id;
  data[bite.layer.Status.Data.ACTION] = bite.layer.Status.Action_.RESET;

  var type = bite.layer.StatusEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.RESET, data);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the close button is pressed.
 * @private
 */
bite.layer.Status.prototype.handleOnResetManager_ = function() {
  var type = bite.layer.StatusEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.RESET_MANAGER);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the close button is pressed.
 * @private
 */
bite.layer.Status.prototype.handleOnRestoreDefaults_ = function() {
  var type = bite.layer.StatusEventType;
  var eventOut =
      new bite.common.events.DataBearingEvent(type.RESTORE_DEFAULTS);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the ondemand checkbox is clicked.
 * @param {string} id The id associated with the pressed button.
 * @param {Event} event The event sent when the checkbox is clicked.
 * @private
 */
bite.layer.Status.prototype.handleSetOndemand_ = function(id, event) {
  var value = event.target.checked;

  var data = {};
  data[bite.layer.Status.Data.ID] = id;
  data[bite.layer.Status.Data.ACTION] =
      bite.layer.Status.Action_.CHANGE_ONDEMAND;
  data[bite.layer.Status.Data.VALUE] =
      value ? bite.layer.Status.State_.ON : bite.layer.Status.State_.OFF;

  var type = bite.layer.StatusEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.SET_ONDEMAND,
                                                         data);
  this.dispatchEvent(eventOut);
};


/**
 * Handler that is called when the state checkbox is clicked.
 * @param {string} id The id associated with the pressed button.
 * @param {Event} event The event sent when the checkbox is clicked.
 * @private
 */
bite.layer.Status.prototype.handleSetState_ = function(id, event) {
  // TODO (jasonstredwick): Refactor and create common code with Ondemand
  // versions.
  var value = event.target.checked;

  var data = {};
  data[bite.layer.Status.Data.ID] = id;
  data[bite.layer.Status.Data.ACTION] =
      bite.layer.Status.Action_.CHANGE_STATE;
  data[bite.layer.Status.Data.VALUE] =
      value ? bite.layer.Status.State_.ON : bite.layer.Status.State_.OFF;

  var type = bite.layer.StatusEventType;
  var eventOut = new bite.common.events.DataBearingEvent(type.SET_STATE, data);
  this.dispatchEvent(eventOut);
};


/**
 * Initialize the Status Console by creating the model and hooking
 * functions to the model.  If a baseUrl is given and the view is not yet
 * initialized then this function will try to initialize it.  Nothing happens
 * if the console is already initialized.
 * @param {string=} opt_baseUrl The baseUrl required for relative references.
 * @return {boolean} Whether or not the model is initialized.  It will also
 *     return true if the model is already initialized.
 * @export
 */
bite.layer.Status.prototype.init = function(opt_baseUrl) {
  if (goog.isDef(opt_baseUrl) && !this.isViewInitialized_) {
    this.initView(opt_baseUrl);
  }

  if (this.isModelInitialized_) {
    return true;
  }

  var helper = bite.common.mvc.helper;
  this.console_ = helper.initModel(bite.layer.soy.Status.getModel);
  if (this.console_ &&
      this.setupBehavior_(this.console_) &&
      this.setupButtonHandlers_(this.console_) &&
      this.setupUsefulElements_(this.console_)) {
    this.isModelInitialized_ = true;
  } else {
    this.undoSetup_();
  }

  return this.isModelInitialized_;
};


/**
 * Given an optional baseUrl, initialize the view for the console by adding
 * its css to the page.
 * @param {string=} opt_baseUrl The baseUrl required for relative references.
 * @return {boolean} Whether or not the view was added.  It also returns true
 *     if the view is already initialized.
 * @export
 */
bite.layer.Status.prototype.initView = function(opt_baseUrl) {
  var helper = bite.common.mvc.helper;
  if (!this.isViewInitialized_ &&
      helper.initView(bite.layer.soy.Status.getView, opt_baseUrl)) {
    this.baseUrl_ = opt_baseUrl || '';
    this.isViewInitialized_ = true;
  }

  return this.isViewInitialized_;
};


/**
 * Determines whether or not the console is initialized and ready for use.
 * @return {boolean} See function comment.
 * @export
 */
bite.layer.Status.prototype.isInitialized = function() {
  return this.isViewInitialized_ && this.isModelInitialized_;
};


/**
 * Complete rebuild the table with the given data.
 * @param {Array.<{id: string, dataStatus: string, loadImmediate: string,
 *                 state: string, status: string}>=} data The data used to
 *     update the rows.
 * @return {boolean} Whether or not the console was updated.
 * @export
 */
bite.layer.Status.prototype.reset = function(data) {
  if (!this.isInitialized()) {
    return false;
  }

  // Clear table
  this.tbody_.innerHTML = '';

  // Repopulate table
  var len = (data && data.length) ? data.length : 0;
  for (var i = 0; i < len; ++i) {
    if (!this.update(data[i])) {
      this.tbody_.innerHTML = '';
      return false;
    }
  }

  return true;
};


/**
 * Setup an element to be controlled by the behaviors.
 * @param {!Element} srcElement The element to control.
 * @return {boolean} Setup was successful.
 * @private
 */
bite.layer.Status.prototype.setupBehavior_ = function(srcElement) {
  var obj = /** @type {Object} */ (this);
  obj.setBehavior(srcElement);
  obj.hide();
  obj.disable();

  return true;
};


/**
 * Setup buttons in console.
 * @param {!Element} srcElement The element to search from.
 * @return {boolean} Whether or not the events were successfully attached to
 *     the button handlers.
 * @private
 */
bite.layer.Status.prototype.setupButtonHandlers_ = function(srcElement) {
  var helper = bite.common.mvc.helper;
  var button = bite.layer.Status.Button_;

  var close = helper.getElement(button.CLOSE, srcElement);
  goog.events.listen(close, goog.events.EventType.CLICK,
                     goog.bind(this.handleOnClose_, this));

  var resetManager = helper.getElement(button.RESET_MANAGER, srcElement);
  goog.events.listen(resetManager, goog.events.EventType.CLICK,
                     goog.bind(this.handleOnResetManager_, this));

  var restoreDefaults = helper.getElement(button.RESTORE_DEFAULTS, srcElement);
  goog.events.listen(restoreDefaults, goog.events.EventType.CLICK,
                     goog.bind(this.handleOnRestoreDefaults_, this));

  return true;
};


/**
 * Locate useful elements within the DOM and record their Ref for later use.
 * @param {!Element} srcElement The element to search from.
 * @return {boolean}  Whether or not the table body element was found.
 * @private
 */
bite.layer.Status.prototype.setupUsefulElements_ = function(srcElement) {
  var helper = bite.common.mvc.helper;
  this.tbody_ = helper.getElement(bite.layer.Status.Table_.TBODY, srcElement);

  return true;
};


/**
 * Reset the model portion of the controller back to an uninitialized state.
 * @private
 */
bite.layer.Status.prototype.undoSetup_ = function() {
  // Make sure and remove the DOM Element from the document upon failure.
  if (this.console_) {
    bite.common.mvc.helper.removeElementById(bite.layer.Status.CONSOLE_ID_);
  }

  this.tbody_ = null;
  this.console_ = null;
  this.isModelInitialized_ = false;
};


/**
 * Using a single set of data, update the associated row.
 * @param {{id: string, dataStatus: string, loadImmediate: string,
 *          state: string, status: string}=} data The data used to
 *     update the row.
 * @return {boolean} Whether or not the console was updated.
 * @export
 */
bite.layer.Status.prototype.update = function(data) {
  if (!this.isInitialized() || !this.validateRowData_(data)) {
    return false;
  }

  var tbody = /** @type {!Element} */ (this.tbody_);

  var id = data[bite.layer.Status.Data.ID];

  var rowId = bite.layer.Status.Table_.TR + id;

  // TODO(jasonstredwick): At some point rethink getElement in case there are
  // more than one entry.  This should never be the case but if it is then
  // the issue would be compounded by just adding more.
  var row = bite.common.mvc.helper.getElement(rowId, tbody) ||
            this.createRow_(id, tbody);
  if (!row) {
    return false;
  }

  var helper = bite.common.mvc.helper;
  var inputBase = bite.layer.Status.InputBase_;
  var image = helper.getElement(inputBase.IMAGE + id, row);
  var ondemand = helper.getElement(inputBase.ONDEMAND + id, row);
  var state = helper.getElement(inputBase.STATE + id, row);
  if (goog.isNull(image) || goog.isNull(ondemand) || goog.isNull(state)) {
    return false;
  }

  if (data[bite.layer.Status.Data.LOAD_IMMEDIATE]) {
    var checked = false;
    var disabled = false;

    // validateRowData ensures that the data is valid.
    switch (data[bite.layer.Status.Data.LOAD_IMMEDIATE]) {
      case bite.layer.Status.State_.NEVER:
        disabled = true;
        break;
      case bite.layer.Status.State_.OFF:
        break;
      case bite.layer.Status.State_.ON:
        checked = true;
        break;
    }
    ondemand.checked = checked;
    ondemand.disabled = disabled;
  }

  if (data[bite.layer.Status.Data.STATE]) {
    checked = false;
    disabled = false;

    // validateRowData ensures that the data is valid.
    switch (data[bite.layer.Status.Data.STATE]) {
      case bite.layer.Status.State_.ALWAYS:
        checked = true;
        disabled = true;
        break;
      case bite.layer.Status.State_.OFF:
        break;
      case bite.layer.Status.State_.ON:
        checked = true;
        break;
    }
    state.checked = checked;
    state.disabled = disabled;
  }

  var dataStatus = data[bite.layer.Status.Data.DATA_STATUS];
  var status = data[bite.layer.Status.Data.STATUS];

  if (status == bite.layer.Status.LayerStatus_.LOADED) {
    if (dataStatus == bite.layer.Status.LayerStatus_.LOADED) {
      image.src = this.baseUrl_ + 'imgs/layerstate-loaded-16.png';
    } else if (dataStatus == bite.layer.Status.LayerStatus_.INIT) {
      image.src =
          this.baseUrl_ + 'imgs/layerstate-unloaded-16.png';
    } else {
      image.src = this.baseUrl_ + 'imgs/layerstate-warning-16.png';
    }
  } else {
    image.src = this.baseUrl_ + 'imgs/layerstate-error-16.png';
  }

  return true;
};


/**
 * Validate the data for a single row, as it is coming from an external
 * source.
 * @param {{id: string, dataStatus: string, loadImmediate: string,
 *          state: string, status: string}=} data The data to be
 *     validated.
 * @return {boolean} Whether or not the data is valid.
 * @private
 */
bite.layer.Status.prototype.validateRowData_ = function(data) {
  if (data && bite.layer.Status.Data.ID in data) {
    if (data[bite.layer.Status.Data.STATUS]) {
      var status = data[bite.layer.Status.Data.STATUS];
      switch (status) {
        case bite.layer.Status.LayerStatus_.BAD_OBJECT:
        case bite.layer.Status.LayerStatus_.FAILED:
        case bite.layer.Status.LayerStatus_.INIT:
        case bite.layer.Status.LayerStatus_.LOADED:
        case bite.layer.Status.LayerStatus_.LOADING:
          break;
        default:
          console.log('bite.layer.Status.validateRowData_(ERROR): Status ' +
                      'value is invalid - ' + status);
          return false;
      }
    }

    if (data[bite.layer.Status.Data.DATA_STATUS]) {
      var dataStatus = data[bite.layer.Status.Data.DATA_STATUS];
      switch (dataStatus) {
        case bite.layer.Status.LayerStatus_.BAD_OBJECT:
        case bite.layer.Status.LayerStatus_.FAILED:
        case bite.layer.Status.LayerStatus_.INIT:
        case bite.layer.Status.LayerStatus_.LOADED:
        case bite.layer.Status.LayerStatus_.LOADING:
          break;
        default:
          console.log('bite.layer.Status.validateRowData_(ERROR): ' +
                      'DataStatus value is invalid - ' + dataStatus);
          return false;
      }
    }

    if (data[bite.layer.Status.Data.LOAD_IMMEDIATE]) {
      var ondemand = data[bite.layer.Status.Data.LOAD_IMMEDIATE];
      switch (ondemand) {
        case bite.layer.Status.State_.NEVER:
        case bite.layer.Status.State_.OFF:
        case bite.layer.Status.State_.ON:
          break;
        default:
          return false;
      }
    }

    if (data[bite.layer.Status.Data.STATE]) {
      var state = data[bite.layer.Status.Data.STATE];
      switch (state) {
        case bite.layer.Status.State_.ALWAYS:
        case bite.layer.Status.State_.OFF:
        case bite.layer.Status.State_.ON:
          break;
        default:
          return false;
      }
    }

    return true;
  }

  return false;
};

